<?php

 /**
  * @package JooS
  * @subpackage PHPackager
  */

  class JooS_PHPackager_Manager {
    const PHPACKAGE_XML = "phpackage.xml";

    const STAGE_ADD_PRE_INSTALL = "pre-install";

    const STAGE_ADD_PRE_UPGRADE = "pre-upgrade";

    const STAGE_ADD_INSTALL = "install";

    const STAGE_ADD_POST_INSTALL = "post-install";

    const STAGE_ADD_POST_UPGRADE = "post-upgrade";

    const STAGE_ADD_FAILED_INSTALL = "failed-install";

    const STAGE_DELETE_PRE_REMOVE = "pre-remove";

    const STAGE_DELETE_PRE_UPGRADE = "pre-upgrade";

    const STAGE_DELETE_REMOVE = "remove";

    const STAGE_DELETE_POST_REMOVE = "post-remove";

    const STAGE_DELETE_POST_UPGRADE = "post-upgrade";

    const STAGE_DELETE_ABORT_UPGRADE = "abort-upgrade";

    const CONFIG_PACKAGES_LIST = "PHPackager_List";
    
    /**
     * @var array
     */
    private static $_installedPackages = null;

    /**
     * @var array
     */
    private $_newPackages = array();

    /**
     * @param string $dir
     */
    public static function addPackages($dir) {
      require_once "JooS/Event/Phpackager/Add/Before.php";
      require_once "JooS/Event/Phpackager/Add/After.php";

      JooS_Event_Phpackager_Add_Before::getInstance()
        ->notify();

      try {
        self::startTransaction();

        /* @var JooS_PHPackager_Manager $manager */
        $manager = new self();
        $manager->make($dir);
        $manager->install();

        self::commitTransaction();
      }
      catch (Exception $e) {
        self::rollbackTransaction();
        throw $e;
      }

      JooS_Event_Phpackager_Add_After::getInstance()
        ->notify();
    }

    /**
     * @param string $name
     */
    public static function removePackage($name) {
      require_once "JooS/Event/Phpackager/Remove/Before.php";
      require_once "JooS/Event/Phpackager/Remove/After.php";

      JooS_Event_Phpackager_Remove_Before::getInstance()
        ->notify();

      try {
        self::startTransaction();

        require_once "JooS/Syntax/Compiler.php";
        require_once "JooS/Syntax/PHPackager/DOM/Name.php";

        $systemName = JooS_Syntax_Compiler::newInstance( new JooS_Syntax_PHPackager_DOM_Name() )
          ->compile($name);

        if ($systemName === false) {
          require_once "JooS/PHPackager/Exception.php";

          throw new JooS_PHPackager_Exception("Bad package name '$name'");
        }

        /* @var JooS_PHPackager_Manager $manager */
        $manager = new self();
        $result = $manager->remove($systemName);

        self::commitTransaction();
      }
      catch (Exception $e) {
        self::rollbackTransaction();
        throw $e;
      }

      try {
        JooS_Event_Phpackager_Remove_After::getInstance()
          ->setResult($result)
          ->notify();
      }
      catch (JooS_Event_Exception $e) {
      }
    }

    /**
     * 
     */
    protected static function startTransaction() {
      set_error_handler(function($errno, $errstr, $errfile) {
        require_once "JooS/PHPackager/Exception.php";

        throw new JooS_PHPackager_Exception("PHP: '$errstr' in '$errfile'", $errno);
      }, E_ALL | E_STRICT | E_RECOVERABLE_ERROR | E_DEPRECATED | E_USER_DEPRECATED);

      require_once "JooS/Config.php";
      require_once "JooS/Event/Config/Commit.php";
      require_once "JooS/Event/Event/Config/Commit.php";
      require_once "JooS/PHPackager/FS.php";

      $root = JooS_Config::PHPackager()->path();
      JooS_PHPackager_FS::startTransaction($root);

      JooS_Event_Config_Commit::getInstance()
        ->attach(array(__CLASS__, "configCommit"));

      JooS_Event_Event_Config_Commit::getInstance()
        ->attach(array(__CLASS__, "observersCommit"));
    }

    /**
     * @deprecated
     */
    protected static function commitTransaction() {
      self::checkIntegrity();

      require_once "JooS/Event/Event/Config/Commit.php";
      require_once "JooS/Event/Config/Commit.php";
      require_once "JooS/PHPackager/FS.php";

      JooS_Event_Event_Config_Commit::getInstance()
        ->detach(array(__CLASS__, "observersCommit"));

      JooS_Event_Config_Commit::getInstance()
        ->setName(self::CONFIG_PACKAGES_LIST)
        ->setData(self::$_installedPackages->valueOf())
        ->notify();

      JooS_Event_Config_Commit::getInstance()
        ->detach(array(__CLASS__, "configCommit"));
      
      JooS_PHPackager_FS::commitTransaction();

      restore_error_handler();
    }

    /**
     * 
     */
    protected static function rollbackTransaction() {
      restore_error_handler();
    }

    /**
     *
     */
    protected static function checkIntegrity() {
      require_once "JooS/Loader.php";

      $manager = new self();

      foreach (self::$_installedPackages as $name) {
        $className = $name->valueOf();

        if (JooS_Loader::loadClass($className)) {
          $package = new $className();
        }
        else {
          require_once "JooS/PHPackager/Exception.php";

          throw new JooS_PHPackager_Exception("Package $className not found...");
        }

        $manager
          ->checkDepends($package)
          ->checkConflicts($package)
          ->checkBreaks($package);
      }
    }

    /**
     * @param JooS_Event_Config_Commit $event
     */
    public static function configCommit(JooS_Event_Config_Commit $event) {
      $name = $event->name;
      $data = $event->data;

      require_once "JooS/PHPackager/FS.php";
      require_once "JooS/Config.php";

      if (is_null($data)) {
        $data = JooS_Config::getInstance($name)
          ->valueOf();
      }

      if (JooS_PHPackager_FS::inTransaction()) {
        $fileSystem = JooS_PHPackager_FS::getInstance();

        require_once "JooS/Loader.php";
        require_once "JooS/CodeGenerator/Class.php";
        require_once "JooS/CodeGenerator/Method.php";
        require_once "JooS/CodeGenerator/Property.php";

        $className = JooS_Config::getClassName($name);

        $configData = JooS_CodeGenerator_Property::newInstance("_staticData")
          ->setStatic(true)
          ->setVisibility( JooS_CodeGenerator_Property::VISIBILITY_PRIVATE )
          ->setValue($data);

        $configConstructor = JooS_CodeGenerator_Method::newInstance("__construct")
          ->setVisibility( JooS_CodeGenerator_Method::VISIBILITY_PROTECTED )
          ->setBody("      parent::__construct(self::\$_staticData);");

        $configClass = JooS_CodeGenerator_Class::newInstance($className)
          ->setExtendedClass("JooS_Config")
          ->addProperty($configData)
          ->addMethod($configConstructor);

        $configPath = JooS_Loader::getRelativePath($className);
        $configRealPath = JooS_Loader::getRealPath($className);

        $fileSystem->delete($configRealPath);
        $fileSystem->file_put_contents($configPath, $configClass->compile());
      }
    }

    /**
     * @param JooS_Event_Event_Config_Commit $event
     */
    public static function observersCommit(JooS_Event_Event_Config_Commit $event) {
      $e = $event->event;

      require_once "JooS/Event/Config/Commit.php";

      JooS_Event_Config_Commit::getInstance()
        ->setName($e->name())
        ->setData($e->observers())
        ->notify();
    }

    /**
     * 
     */
    protected function __construct() {
      if (self::$_installedPackages === null)
        self::$_installedPackages = JooS_Config::getInstance(self::CONFIG_PACKAGES_LIST);
    }

    /**
     * @param string $dir
     * @return JooS_PHPackager_Manager
     */
    protected function make($dir) {
      require_once "JooS/Loader.php";
      require_once "JooS/Config.php";
      require_once "JooS/PHPackager/File.php";
      require_once "JooS/PHPackager/FS.php";
      require_once "JooS/PHPackager/DOM/Node.php";

      require_once "JooS/Event/Phpackager/Package/Instance.php";
      require_once "JooS/Event/Phpackager/Package/Source.php";

      if (is_string($dir))
        $dir = array($dir);
      $path = null;

      $_packagesNew = &$this->_newPackages;

      $Event_newPackage_observer = function(JooS_Event_Phpackager_Package_Source $event) use (&$path, &$_packagesNew) {
        $className = $event->className;
        $package = $event->package;

        $packageSource = $package->compile();

        $adapter = JooS_PHPackager_File::createAdapter("Tmp")
          ->file_put_contents($packageSource)
          ->include_file();

        JooS_PHPackager_FS::getInstance()
          ->copy( $adapter->get_name(), JooS_Loader::getRelativePath($className) );

        $newPackage = new $className();
        $newPackage->setInstallPath($path);
        $newPackage->createArchive();

        $_packagesNew[$newPackage->getName()] = $newPackage;

        JooS_Event_Phpackager_Package_Instance::getInstance()
          ->setPackage($newPackage)
          ->notify();
      };

      JooS_Event_Phpackager_Package_Source::getInstance()
        ->attach($Event_newPackage_observer);

      foreach ($dir as $path) {
        $path = realpath($path);

        $document = new DOMDocument("1.0", "UTF-8");
        $document->load( $path . DIRECTORY_SEPARATOR . self::PHPACKAGE_XML );

        JooS_PHPackager_DOM_Node::newInstance($document->documentElement, null, "JooS_PHPackager_DOM_Element_Control")
          ->make()
          ->install();
      }

      JooS_Event_Phpackager_Package_Source::getInstance()
        ->detach($Event_newPackage_observer);

      return $this;
    }

    /**
     * @return JooS_PHPackager_Manager
     */
    protected function install() {
      require_once "JooS/Loader.php";
      require_once "JooS/Config.php";
      require_once "JooS/PHPackager/FS.php";
      require_once "JooS/PHPackager/Instance.php";

      require_once "JooS/Event/Config/Commit.php";
      require_once "JooS/Event/Phpackager/Package/Installed.php";
      require_once "JooS/Event/Phpackager/Package/Upgraded.php";
      require_once "JooS/Event/Phpackager/Package/Commited.php";

      foreach ($this->_newPackages as $className => $NEW_package) {
        $this
          ->checkBreaks($NEW_package)
          ->checkConflicts($NEW_package)
          ->checkDepends($NEW_package);

        $NEW_version = $NEW_package->getVersion();

        $OLD_package = $this->getPackageByName($NEW_package->getName());
        $OLD_version = $OLD_package ? $OLD_package->getVersion() : null;

        try {
          if ($OLD_package === null) {
            $NEW_package->add( self::STAGE_ADD_PRE_INSTALL );
            $NEW_package->add( self::STAGE_ADD_INSTALL );
            $NEW_package->add( self::STAGE_ADD_POST_INSTALL );

            JooS_Event_Phpackager_Package_Installed::getInstance()
              ->setPackage($NEW_package)
              ->notify();
          }
          else {
            $OLD_package->delete( self::STAGE_DELETE_PRE_UPGRADE, $NEW_version );
            $NEW_package->add( self::STAGE_ADD_PRE_UPGRADE, $OLD_version );
            $OLD_package->delete( self::STAGE_DELETE_REMOVE, $NEW_version );
            $OLD_package->delete( self::STAGE_DELETE_POST_UPGRADE, $NEW_version );
            $NEW_package->add( self::STAGE_ADD_INSTALL, $OLD_version );
            $NEW_package->add( self::STAGE_ADD_POST_UPGRADE, $OLD_version );

            JooS_Event_Phpackager_Package_Upgraded::getInstance()
              ->setNEW_package($NEW_package)
              ->setOLD_package($OLD_package)
              ->notify();
          }
        }
        catch (Exception $e) {
          restore_error_handler();

          if ($OLD_package !== null)
            $OLD_package->delete( self::STAGE_DELETE_ABORT_UPGRADE, $NEW_version );
          $NEW_package->add( self::STAGE_ADD_FAILED_INSTALL, $OLD_version );

          throw $e;
        }

        JooS_Event_Config_Commit::getInstance()
          ->setName($NEW_package->getEnvironmentConfigName())
          ->setData($NEW_package->getEnvironment())
          ->notify();
      }

      foreach ($this->_newPackages as $className => $package) {
        $archive = $package->getArchive();
        $realPath = $archive->getAdapter()->get_name();
        $archive->close();

        JooS_PHPackager_FS::getInstance()
          ->copy($realPath, JooS_Loader::getRelativePath(get_class($package), "zip"));

        self::$_installedPackages[$package->getName()] = get_class($package);

        JooS_Event_Phpackager_Package_Commited::getInstance()
          ->setPackage($package)
          ->notify();
      }

      return $this;
    }

    /**
     *
     * @param string $name
     * @param string $operator
     * @param string $version
     * @return bool
     */
    protected function remove($name, $operator = null, $version = null) {
      $OLD_package = $this->getPackageByName($name, $operator, $version);
      if ($OLD_package) {
        $OLD_package->delete( self::STAGE_DELETE_PRE_REMOVE );
        $OLD_package->delete( self::STAGE_DELETE_REMOVE );
        $OLD_package->delete( self::STAGE_DELETE_POST_REMOVE );

        unset(self::$_installedPackages[$name]);
        return true;
      }
      else {
        return false;
      }
    }

    /**
     * @param string $name
     * @param string $operator
     * @param string $version
     * @param string $installed
     * @return JooS_PHPackager_Instance
     */
    protected function getPackageByName($name, $operator = null, $version = null, $installed = true) {
      /* @var $package JooS_PHPackager_Instance */
      if (!$installed && isset($this->_newPackages[$name])) {
        $package = $this->_newPackages[$name];
        $className = get_class($package);
      }
      elseif (isset(self::$_installedPackages[$name])) {
        $className = self::$_installedPackages[$name]->valueOf();

        if (JooS_Loader::loadClass($className)) {
          $package = new $className();
        }
        else {
          require_once "JooS/PHPackager/Exception.php";

          throw new JooS_PHPackager_Exception("Already installed package '$name' not found");
        }
      }
      else
        return null;

      if ($operator !== null && $version !== null) {
        if (version_compare($package->getVersion(), $version, $operator)) {
          return $package;
        }
      }
      else {
        return $package;
      }

      return null;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager 
     */
    protected function checkDepends(JooS_PHPackager_Instance $package) {
      foreach ($package->getDepends() as $relation) {
        switch (true) {
          /* @var $relation JooS_PHPackager_Relation_Php_Interface */
          case ($relation instanceof JooS_PHPackager_Relation_Php_Interface) &&
               !$relation->exists():
          /* @var $relation JooS_PHPackager_Relation_Package_Interface */
          case ($relation instanceof JooS_PHPackager_Relation_Package_Interface) &&
               !$this->getPackageByName($relation->getName(), $relation->getOperator(), $relation->getVersion(), false):
            require_once "JooS/PHPackager/Exception.php";
            
            throw new JooS_PHPackager_Exception("checkDepends: " . $relation->getMessage());
        }
      }
      return $this;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager
     */
    protected function checkBreaks(JooS_PHPackager_Instance $package) {
      foreach ($package->getBreaks() as $relation) {
        switch (true) {
          /* @var $relation JooS_PHPackager_Relation_Package_Interface */
          case ($relation instanceof JooS_PHPackager_Relation_Package_Interface) &&
               $this->getPackageByName($relation->getName(), $relation->getOperator(), $relation->getVersion(), true):
            require_once "JooS/PHPackager/Exception.php";

            throw new JooS_PHPackager_Exception("checkBreaks: " . $relation->getMessage());
        }
      }
      return $this;
    }

    /**
     * @param JooS_PHPackager_Instance $package
     * @return JooS_PHPackager_Manager
     */
    protected function checkConflicts(JooS_PHPackager_Instance $package) {
      foreach ($package->getConflicts() as $relation) {
        switch (true) {
          /* @var $relation JooS_PHPackager_Relation_Package_Interface */
          case ($relation instanceof JooS_PHPackager_Relation_Package_Interface) &&
               $this->getPackageByName($relation->getName(), $relation->getOperator(), $relation->getVersion(), true):
            require_once "JooS/PHPackager/Exception.php";

            throw new JooS_PHPackager_Exception("checkConflicts: " . $relation->getMessage());
        }
      }
      return $this;
    }
  }
